02 / 09

How does memory management work in JS engines?

JavaScript engines manage memory automatically through a combination of stack allocation for primitive values and function calls, and heap allocation with generational garbage collection for objects.

JavaScript engines like V8 (Chrome/Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) handle memory management automatically, freeing developers from manual allocation and deallocation. The memory model is divided into two primary regions: the stack and the heap, each with distinct characteristics and purposes. The heap is further organized using a generational approach to optimize garbage collection based on object lifetimes .

Stack Memory: Fast and Automatic
  1. 1

    Purpose: The stack stores primitive values (numbers, booleans, strings, null, undefined, symbols) and references to heap objects, along with function call frames .

  2. 2

    Management: Stack memory operates on a Last-In-First-Out (LIFO) basis. Each function call creates a new stack frame that is automatically destroyed when the function returns .

  3. 3

    Performance: Stack allocation is extremely fast because it only involves moving a pointer. However, stack size is limited (typically ~1-8 MB per thread), and deep recursion can cause stack overflow errors .

Heap Memory: Dynamic and Garbage-Collected
  1. 1

    Purpose: The heap stores all objects, arrays, functions, and closures—anything that requires dynamic memory allocation .

  2. 2

    Generational Design: Modern engines use generational garbage collection based on the observation that most objects die young. The heap is divided into generations with different collection strategies .

  3. 3

    Heap Structure in V8: V8 organizes the heap into several regions including: New Space (young generation), Old Space (old generation), Code Space (compiled code), Map Space (hidden classes), and Large Object Space (objects too large for other spaces) .

Monitoring Memory Usage in Node.js

The generational garbage collection strategy is fundamental to JavaScript engine performance. Engines assume that most objects are temporary and die young, so they optimize for this common case by using different algorithms for different generations .

Young Generation (New Space / Nursery)
  1. 1

    Purpose: Newly created objects are allocated here. This space is small (typically 1-8 MB) and collected frequently .

  2. 2

    Scavenge Algorithm: V8 uses a semi-space copying collector. The young generation is split into two equal halves: From-space (active) and To-space (inactive). New objects go into From-space. During collection, live objects are copied to To-space, then the spaces are swapped .

  3. 3

    SpiderMonkey's Nursery: Firefox's engine uses a similar concept called the 'nursery' for short-lived objects. During nursery collection, accessible objects are 'tenured' (moved to long-lived memory) and the nursery is cleared .

  4. 4

    Performance Trade-off: Scavenge is fast because it only processes live objects and doesn't need to scan the entire heap, but it wastes half the space as idle .

Visualizing Young Generation Collection
Old Generation (Old Space / Tenured)
  1. 1

    Promotion: Objects that survive multiple young generation collections (typically 2 cycles) are moved (promoted) to the old generation .

  2. 2

    Mark-Sweep Algorithm: The old generation uses mark-sweep collection. It starts from root objects (global object, stack variables, etc.), traverses the object graph, and marks all reachable objects. Unmarked objects are considered garbage and their memory is freed .

  3. 3

    Mark-Compact: To combat memory fragmentation, mark-compact moves all live objects together, then frees the remaining space as one contiguous block. This is more expensive but necessary for large heaps .

  4. 4

    Incremental Marking: To avoid long 'stop-the-world' pauses, V8 uses incremental marking, breaking the marking phase into small steps interleaved with program execution—similar conceptually to React Fiber .

Garbage Collection Algorithms in Detail
  1. 1

    Mark-and-Sweep: The fundamental algorithm. Starting from root objects (global object, current stack, registers), it traverses all references and marks every reachable object. Then it sweeps through memory, freeing unmarked objects. This handles circular references correctly .

  2. 2

    Reference Counting: An older approach that tracks how many references point to each object. When count reaches zero, the object is freed. However, it fails with circular references (two objects referencing each other with no external references) and isn't used alone in modern engines .

  3. 3

    Generational Collection: Combines the above with the insight that most objects die young. Young generation collected frequently with copying collector; old generation collected less frequently with mark-sweep/compact .

Promotion Threshold Example
Memory Management Across Different Engines
  1. 1

    V8 (Chrome/Node.js): Uses Ignition interpreter and TurboFan compiler. Implements generational GC with Scavenge for young generation and mark-sweep/compact for old generation. Provides command-line flags like --max-old-space-size and --max-semi-space-size for tuning .

  2. 2

    SpiderMonkey (Firefox): Also uses generational GC with a nursery for young objects. Has special handling for strings (deduplication, atomization) and uses a mark-and-sweep collector for the tenured heap. Embedders using the C++ API must use rooting mechanisms (JS::Rooted) to protect objects from garbage collection .

  3. 3

    JavaScriptCore (Safari): Provides memory management through its JSVirtualMachine and JSManagedValue classes, with conditional retain behavior for automatic management. Can detect system RAM size to set allocation limits .

Common Memory Leak Patterns
  1. 1

    Global Variables: Variables attached to the global object remain reachable forever. Always use let, const, or module scope .

  2. 2

    Forgotten Event Listeners: Listeners keep references to their callback functions and any variables closed over them. Remove listeners when no longer needed .

  3. 3

    Closures: Inner functions that close over variables keep those variables alive as long as the function exists. Be mindful of what closures capture .

  4. 4

    Detached DOM Elements: Holding references to removed DOM nodes prevents their memory from being freed .

  5. 5

    Circular References: While modern GC handles cycles, circular references in combination with other patterns (like event listeners) can still cause leaks .

Understanding memory management helps developers write more efficient code. Tools like Chrome DevTools Memory panel and Node.js flags (--trace-gc, --expose-gc for manual triggering) allow inspection of memory behavior . Key takeaways: keep object shapes consistent (monomorphic) to help hidden classes, avoid creating many temporary objects in hot paths, and be mindful of closures that inadvertently capture large objects. The engine optimizes automatically, but informed developers can avoid patterns that force deoptimization or prevent efficient garbage collection .

Difficulty: 5/10
Topics: garbage collection, heap allocation, reference cycles

Scenario Questions

0-2 years experience
  1. 1

    If you create a large array inside a function and never return it, what happens to that memory after the function finishes?

  2. 2

    How would you avoid a memory leak when adding event listeners in a single‑page app?

  3. 3

    After you set a variable to null, does the memory get freed immediately? Why or why not?

2-5 years experience
  1. 1

    We noticed a spike in memory usage after deploying a new feature that caches API responses. Walk me through how you'd investigate whether the JavaScript garbage collector is the cause.

  2. 2

    Explain why a closure that captures a DOM element can cause memory to grow over time, and how you'd refactor it.

  3. 3

    Our Node.js service occasionally pauses for a few seconds; profiling points to GC pauses. What trade‑offs would you consider when tuning V8's heap size?

5-8 years experience
  1. 1

    Design a client‑side caching layer for a high‑traffic web app that must stay within a strict memory budget. How would you use V8's memory management features to enforce limits and avoid leaks?

  2. 2

    When migrating a legacy codebase to a newer V8 version, what risks related to garbage collection should you anticipate, and how would you mitigate them at the system level?

  3. 3

    Explain how you would instrument a large React application to detect hidden memory leaks caused by retained component references.

8+ years experience
  1. 1

    Our company is evaluating moving from a Node.js monolith to a micro‑frontend architecture that runs many independent JS runtimes in the same browser tab. What architectural considerations around memory isolation and garbage collection would you raise?

  2. 2

    If we need to support a long‑running web worker that processes streaming data for days, how would you design its memory management strategy to prevent heap bloat and ensure predictable performance?

  3. 3

    Discuss the trade‑offs of using manual object‑pooling techniques versus relying on V8's GC in a high‑frequency trading front‑end.

Follow-up Questions

  • What tooling would you use to inspect GC pauses in a browser?
  • How can you tell from a heap snapshot that a closure is retaining unwanted data?
  • When would you consider tuning V8's heap size versus refactoring code?